joe di castrohttp://joedicastro.com2011-04-28T14:04:00+02:00Comprobar si un programa está instalado con Python2011-04-28T14:04:00+02:00joe di castrohttp://joedicastro.com/comprobar-si-un-programa-esta-instalado-con-python.html<p>Cuando creamos un script en <strong>Python</strong>, sobre todo aquellos orientados a
ejecutarse en la línea de comandos, a veces necesitamos echar mano de un
programa externo que no siempre viene por defecto instalado en el sistema. Por
ejemplo, en el script que empleo en
<a href="http://joedicastro.com/optimizar_imagenes_para_la_web">optimizar imágenes para la web</a>
empleo los programas externos <em>pngcrush</em> y <em>jpegtran</em>. ¿Como comprobamos
entonces si el programa está instalado? Desde luego es siempre mejor
comprobarlo y avisar al usuario, que dejar que arroje un feo error.</p>
<p>Una forma de comprobar si el programa está instalado es capturando la excepción
cuando se produzca con las sentencias <code>try</code> y <code>except</code>, incluyendo la llamada al
ejecutable dentro de ellas y devolver un mensaje de error avisando de la
necesidad de la presencia de este ejecutable. Pero al igual que en el ejemplo
anterior, yo prefiero comprobar esto incluso antes de ejecutar cualquier otro
código dentro del script. Para ello empleo el <a href="http://ibiblio.org/g2swap/byteofpython/read/module-name.html">conocido truco</a> que nos
permite ejecutar cierto código solo cuando es ejecutado como script y no cuando
es importado como módulo:</p>
<div class="codehilite"><pre><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
<span class="n">chequeo_ejecutable</span><span class="p">()</span>
<span class="n">main</span><span class="p">()</span>
</pre></div>
<p>Donde <code>main()</code> es la función principal donde albergaríamos el código fundamental
del script. Es un conocido <a href="http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html">Python Idiom</a> que me sirve perfectamente para
esta tarea. De hecho lo empleo habitualmente en todos mis scripts para separar
el código principal de las funciones. </p>
<p>Comprobar en Linux si existe el ejecutable es realmente sencillo (siempre y
cuando conozcamos el nombre exacto del ejecutable) con este código:</p>
<div class="codehilite"><pre><span class="c">#!/usr/bin/env python</span>
<span class="c"># -*- coding: utf8 -*-</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">Popen</span><span class="p">,</span> <span class="n">PIPE</span>
<span class="k">def</span> <span class="nf">check_execs</span><span class="p">(</span><span class="o">*</span><span class="n">progs</span><span class="p">):</span>
<span class="sd">"""Check if the programs are installed, if not exit and report."""</span>
<span class="k">for</span> <span class="n">prog</span> <span class="ow">in</span> <span class="n">progs</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">Popen</span><span class="p">([</span><span class="n">prog</span><span class="p">,</span> <span class="s">'--help'</span><span class="p">],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span>
<span class="n">msg</span> <span class="o">=</span> <span class="s">'The {0} program is necessary to run this script'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">prog</span><span class="p">)</span>
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
<span class="k">return</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="c"># Incluir aquí el código fundamental del script</span>
<span class="k">pass</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
<span class="n">check_execs</span><span class="p">(</span><span class="s">'python'</span><span class="p">)</span>
<span class="n">main</span><span class="p">()</span>
</pre></div>
<p>Si no existiera el ejecutable que le pasamos a la función <code>check_execs()</code>
entonces se detendrá el script y nos dirá que necesitamos instalarlo para
ejecutar el script. Hacerlo así nos evita cualquier tipo de manipulación previa
antes de darnos cuenta de que nos falta un elemento esencial para ejecutarlo al
completo. Esta función funciona porque en Linux los ejecutables normalmente
están colgando de una ruta del PATH. En Windows, por ejemplo, la cosa cambia.</p>
<p>Si queremos que el script sea multiplataforma y que nos funcione tanto en
sistemas *NIX como en Windows entonces necesitamos una función algo más
compleja. En Windows un ejecutable no necesita colgar del PATH y es muy
frecuente que no sea así. Al mismo tiempo en Windows podemos tener el sistema de
ficheros repartido en varias unidades de disco, y necesitamos explorar todas
ellas para buscar nuestro ejecutable. Aunque lo más frecuente es que se
encuentre en la unidad C: y la función se detiene en cuanto encuentra el primer
ejecutable, el hecho de realizar está búsqueda hace que el proceso sea más lento
en Windows que en Linux. </p>
<p>La función multiplataforma es la siguiente:</p>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">check_execs_posix_win</span><span class="p">(</span><span class="n">progs</span><span class="p">):</span>
<span class="sd">"""Check if the program is installed.</span>
<span class="sd"> Returns one dictionary with 1+n pair of key/values:</span>
<span class="sd"> A fixed key/value:</span>
<span class="sd"> "WinOS" -- (boolean) True it's a Windows OS, False it's a *nix OS</span>
<span class="sd"> for each program in progs a key/value like this:</span>
<span class="sd"> "program" -- (str or boolean) The Windows executable path if founded else </span>
<span class="sd"> '' if it's Windows OS. If it's a *NIX OS True</span>
<span class="sd"> if founded else False</span>
<span class="sd"> """</span>
<span class="n">execs</span> <span class="o">=</span> <span class="p">{</span><span class="s">'WinOS'</span><span class="p">:</span><span class="bp">True</span> <span class="k">if</span> <span class="n">platform</span><span class="o">.</span><span class="n">system</span><span class="p">()</span> <span class="o">==</span> <span class="s">'Windows'</span> <span class="k">else</span> <span class="bp">False</span><span class="p">}</span>
<span class="c"># get all the drive unit letters if the OS is Windows</span>
<span class="n">windows_drives</span> <span class="o">=</span> <span class="n">findall</span><span class="p">(</span><span class="s">r'(\w:)</span><span class="se">\\</span><span class="s">'</span><span class="p">,</span>
<span class="n">Popen</span><span class="p">(</span><span class="s">'fsutil fsinfo drives'</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span><span class="o">.</span>
<span class="n">communicate</span><span class="p">()[</span><span class="mi">0</span><span class="p">])</span> <span class="k">if</span> <span class="n">execs</span><span class="p">[</span><span class="s">'WinOS'</span><span class="p">]</span> <span class="k">else</span> <span class="bp">None</span>
<span class="n">progs</span> <span class="o">=</span> <span class="p">[</span><span class="n">progs</span><span class="p">]</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">progs</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span> <span class="k">else</span> <span class="n">progs</span>
<span class="k">for</span> <span class="n">prog</span> <span class="ow">in</span> <span class="n">progs</span><span class="p">:</span>
<span class="k">if</span> <span class="n">execs</span><span class="p">[</span><span class="s">'WinOS'</span><span class="p">]:</span>
<span class="c"># Set all commands to search the executable in all drives</span>
<span class="n">win_cmds</span> <span class="o">=</span> <span class="p">[</span><span class="s">'dir /B /S {0}\*{1}.exe'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">letter</span><span class="p">,</span> <span class="n">prog</span><span class="p">)</span> <span class="k">for</span>
<span class="n">letter</span> <span class="ow">in</span> <span class="n">windows_drives</span><span class="p">]</span>
<span class="c"># Get the first location (usually in C:) where the executable exists</span>
<span class="k">for</span> <span class="n">cmd</span> <span class="ow">in</span> <span class="n">win_cmds</span><span class="p">:</span>
<span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">Popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span><span class="o">.</span>
<span class="n">communicate</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">linesep</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span>
<span class="k">if</span> <span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]:</span>
<span class="k">break</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">Popen</span><span class="p">([</span><span class="n">prog</span><span class="p">,</span> <span class="s">'--help'</span><span class="p">],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">PIPE</span><span class="p">)</span>
<span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span>
<span class="n">execs</span><span class="p">[</span><span class="n">prog</span><span class="p">]</span> <span class="o">=</span> <span class="bp">False</span>
<span class="k">return</span> <span class="n">execs</span>
</pre></div>
<p>En la parte de Windows (la de *NIX es básicamente igual) esta función lo que
hace es obtener primero las letras de las unidades de disco disponibles en el
sistema. Luego busca el ejecutable en cada una de ellas y en cuanto encuentra
la primera ruta al ejecutable se detiene. La función en este caso devuelve un
diccionario donde hay una clave fija que es <code>WinOS</code> que será <code>True</code> si estamos
en Windows y <code>False</code> en caso contrario. Luego nos devuelve una clave por cada
uno de los programas que le mandemos comprobar. Esta clave sera <abbr title="Verdadero o Falso. El nombre proviene de la Álgebra de Boole.">booleana</abbr> en el
caso de *NIX y la ruta del programa (o una cadena vacía) en el caso de Windows.</p>
<p>Las funciones y un ejemplo de su funcionamiento podéis encontrarlas en el
fichero <code>check_execs.py</code> en mi repositorio <em>Python Recipes</em> que se encuentra
alojado en <a href="http://github.com/joedicastro/python-recipes">github</a>.</p>